#!/usr/bin/env node

import fs from 'node:fs/promises';
import path from 'node:path';
import caniuse, { feature as unpackFeature } from 'caniuse-lite';
// eslint-disable-next-line import/no-extraneous-dependencies
import caniuseDb from 'caniuse-db/data.json' assert { type: 'json' };
import { kebabToCamel } from '../utils/kebabToCamel.js';

/**
 * @typedef {Object} StubOptions
 * @prop {string} id
 * @prop {string} name
 * @prop {string} description
 * @prop {string} link
 */

/** @param {StubOptions} options */
function generateFeatureStub({ name, description, link }) {
  return `
/**
 * TODO: initially implement feature
 * ${name}
 * ${description}
 * @see ${link}
 * @type {import('../features').Feature}
 */
export default {};
`.trimStart();
}

/** @param {StubOptions} options */
function generateTestStub({
  name, description, link, id,
}) {
  return `
/*
 * This file contains tests for ${name}
 * ${description}
 * 
 * See: ${link}
 * TODO: create a test for this feature and move it to test/cases/unimplemented/
 * TODO: write an implementation for this feature and move it to test/cases/features/
 */

/*
expect:
${id}: 0
*/
`.trimStart();
}

/**
 * @param {Object} options
 * @param {string} options.imports
 * @param {string} options.features
 */
function generateFeatureListFile({ imports, features }) {
  return `
/* THIS FILE IS AUTOGENERATED */

${imports}

/** @typedef {RegExp|string|((value:string) => boolean)} FeatureCheck */

/** @typedef {((rule:import('postcss').ChildNode) => boolean)} RuleCheck */

/** @typedef {Record<string, FeatureCheck|FeatureCheck[]|boolean> | RuleCheck | RuleCheck[]} Feature */

/** @enum {Feature} */
const FEATURES = {
${features}
};

/** @typedef {keyof typeof FEATURES} FeatureKeys */

export default /** @type {{[K in FeatureKeys]: Feature}} */ (FEATURES);
`.trimStart();
}

/**
 * @param {string} filePath
 * @param {string} data
 */
async function writeIfEmpty(filePath, data) {
  try {
    const stats = await fs.stat(filePath);
    if (stats.size > 0) return false;
  } catch { /* empty */ }
  await fs.writeFile(filePath, data);
  return true;
}

/** @type {Map<string, StubOptions>} */
const cssFeatures = new Map();
for (const [featureId, packedFeature] of Object.entries(caniuse.features)) {
  // 'caniuse-db' is maintained by caniuse.com
  // 'caniuse-lite' is maintained by browserslist
  // Some keys are extraneous

  const databaseEntry = (featureId in caniuseDb.data)
    ? caniuseDb.data[featureId]
    : null;

  /**
   * Only process features that:
   *  - have a css category
   *  - have 'CSS' in the feature id
   *  - have 'text-decoration' or 'stylesheet' (MDN)
   */
  if (/css|text-decoration|stylesheet/.test(featureId) === false
      && !databaseEntry?.categories.includes('CSS')
      && !databaseEntry?.categories.includes('CSS2')
      && !databaseEntry?.categories.includes('CSS3')) {
    continue;
  }

  const title = databaseEntry ? databaseEntry.title : unpackFeature(packedFeature)?.title;

  const stubOptions = {
    id: featureId,
    name: title,
    link: databaseEntry
      ? `https://caniuse.com/${featureId}`
      : `https://developer.mozilla.org/en-US/search?q=${encodeURIComponent(title)}`,
    description: databaseEntry?.description
      // replace all non-standard whitespace with normal spaces
      // and nbsp is a special case
      ?.replaceAll(/\s|\u00A0/g, ' ')
      .trim() ?? 'No description available',
  };
  cssFeatures.set(featureId, stubOptions);
}

// for each feature, make sure that data/features/{feature}.js exists
// if not, create it.
await Promise.all(
  [...cssFeatures]
    .map(([id, stubOptions]) => writeIfEmpty(
      path.resolve(`data/features/${id}.js`),
      generateFeatureStub(stubOptions),
    )),
);

// check every file in 'data/features' to make sure it's in the list of features
// if not, delete it
/** @type {string[][]} */
const notCSS = [];
const featureFiles = await fs.readdir('data/features');
await Promise.all(
  featureFiles.map(async (filename) => {
    const name = filename.replace(/\.js$/, '');
    if (cssFeatures.has(name)) return;
    // make sure the file is empty or only contains a TODO comment
    const filepath = path.resolve(`data/features/${filename}`);
    const fileContent = await fs.readFile(filepath, 'utf8');
    
    const feature = caniuse.features[name];
    if (!feature) return; // Ignore removed features
    const { title } = unpackFeature(feature);
    const category = caniuseDb.data[name]?.categories.join(', ');

    if (fileContent.includes(' * TODO: initially implement ')
        || fileContent.trim() === '') {
      await fs.unlink(`data/features/${filename}`);
    } else {
      console.warn(`\u001B[33mWARNING: ${filename}: ${title} isn't a CSS feature.${
        category ? ` It's tagged with ${category}.` : ''
      }\u001B[0m`);
      notCSS.push([filename.replace(/\.js$/, '')]);
    }
  }),
);

const allFeatures = [...cssFeatures, ...notCSS].sort(([a], [b]) => a.localeCompare(b));

// update the data/features.js file with all the features

await fs.writeFile('data/features.js', generateFeatureListFile({
  imports: allFeatures.map(([name]) => `import ${kebabToCamel(name)} from './features/${name}.js';`).join('\n'),
  features: allFeatures.map(([name]) => `  '${name}': ${kebabToCamel(name)},`).join('\n'),
}));

// create test stubs for each feature
const existingTests = await fs.readdir('test/cases/features');
const unimplementedTests = await fs.readdir('test/cases/unimplemented');
const allTests = new Set([...existingTests, ...unimplementedTests]);

await Promise.all(
  allFeatures.map(async ([featureId]) => {
    const filename = `${featureId}.css`;

    if (allTests.has(filename)) return;
    const fullData = caniuseDb.data[featureId];
    const { title } = unpackFeature(caniuse.features[featureId]);
    const stubOptions = {
      id: featureId,
      name: title,
      link: fullData
        ? `https://caniuse.com/${featureId}`
        : `https://developer.mozilla.org/en-US/search?q=${encodeURIComponent(title)}`,
      description: fullData?.description
        // replace all non-standard whitespace with normal spaces
        // and nbsp is a special case
        ?.replaceAll(/\s|\u00A0/g, ' ')
        .trim() ?? 'No description available',
    };

    await fs.writeFile(
      `test/cases/untriaged/${filename}`,
      generateTestStub(stubOptions),
    );
  }),
);

// Give warnings for test cases that don't have a feature
const noFeatures = [];
for (const filename of existingTests) {
  if (!filename.endsWith('.css')) continue;
  const featureId = filename.replace(/\.css$/, '');
  if (cssFeatures.has(featureId)) continue;
  noFeatures.push(filename);
}

if (noFeatures.length > 0) {
  console.warn("The following test cases don't have a corresponding feature:");
  console.warn(noFeatures.join(', '));
}
